package com.tianji.promotion.service.impl;

import cn.hutool.core.bean.copier.CopyOptions;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.autoconfigure.redisson.annotations.Lock;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.NumberUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.constants.PromotionConstants;
import com.tianji.promotion.domain.dto.UserCouponDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.enums.ExchangeCodeStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import com.tianji.promotion.utils.CodeUtil;
import com.tianji.promotion.utils.MyLock;
import com.tianji.promotion.utils.RedisLock;
import lombok.RequiredArgsConstructor;
import org.springframework.aop.framework.AopContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.tianji.promotion.constants.PromotionConstants.COUPON_CODE_MAP_KEY;
import static com.tianji.promotion.constants.PromotionConstants.COUPON_RANGE_KEY;

/**
 * <p>
 * 用户领取优惠券的记录，是真正使用的优惠券信息 服务实现类
 * </p>
 *
 * @author XinxuanZhuo
 * @since 2024-03-20
 */
@SuppressWarnings("ALL")
@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;

    private final IExchangeCodeService codeService;

    private final StringRedisTemplate stringRedisTemplate;

    private final RabbitMqHelper mqHelper;

    // 直接领取优惠券脚本
    private static final RedisScript<Integer> RECEIVE_COUPON_SCRIPT;

    // 兑换码优惠券脚本
    private static final RedisScript<String> EXCHANGE_COUPON_SCRIPT;

    static {
        RECEIVE_COUPON_SCRIPT = RedisScript.of(new ClassPathResource("lua/receive_coupon.lua"));
        EXCHANGE_COUPON_SCRIPT = RedisScript.of(new ClassPathResource("lua/exchange_coupon.lua"));
    }


    /**
     * 领取优惠券接口
     */
//    @Override
////    @Transactional
//    public void receiveCoupon(Long couponId) {
//        //查询优惠券信息
//        Coupon coupon = couponMapper.selectById(couponId);
//        if (coupon == null) {
//            throw new BadRequestException("优惠券不存在");
//        }
//        // 时间校验
//        LocalDateTime now = LocalDateTime.now();
//        if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
//            throw new BadRequestException("优惠券发放已经结束或尚未开始");
//        }
//        // 库存校验
//        if (coupon.getIssueNum() >= coupon.getTotalNum()) {
//            throw new BadRequestException("优惠券库存不足");
//        }
//        // 用户领取校验
//        Long userId = UserContext.getUser();
//        // 4.校验并生成用户券
//        // 4.1单机锁（有问题！）
//        /*
//        synchronized (userId.toString().intern()) { // 这里加锁，这样锁在事务之外
//            // 非事务方法调用事务方法，会导致事务失效
//            IUserCouponService userCouponService = (IUserCouponService) AopContext.currentProxy();
//            userCouponService.checkAndCreateUserCoupon(coupon, userId, null);
//        }
//         */
//
//        // 4.2redis分布式锁（有问题！）
//        /*
//        String key = "lock:coupon:uid:" + userId;
//        RedisLock redisLock = new RedisLock(key, stringRedisTemplate);
//        // 尝试获取锁
//        boolean isLock = redisLock.tryLock(5, TimeUnit.SECONDS);
//        if (!isLock) {
//            throw new BizIllegalException("请求太频繁");
//        }
//        // 获取锁成功执行代码
//        try {
//            IUserCouponService userCouponService = (IUserCouponService) AopContext.currentProxy();
//            userCouponService.checkAndCreateUserCoupon(coupon, userId, null);
//        } finally {
//            redisLock.unlock();
//        }
//         */
//
//        // checkAndCreateUserCoupon 方法 加了redisson分布式锁
//        IUserCouponService userCouponService =  (IUserCouponService)AopContext.currentProxy();
//        userCouponService.checkAndCreateUserCoupon(coupon, userId, null);
//
//    }

    /**
     * 领取优惠券接口
     */
    @Override
    @Lock(name = "lock:coupon:#{couponId}")  // 使用lua脚本不加锁也是安全的
    public void receiveCoupon(Long couponId) {
        Coupon coupon = queryCouponByCache(couponId);
        if (coupon == null) {
            throw new BadRequestException("优惠券不存在");
        }
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
            throw new BadRequestException("优惠券发放已经结束或尚未开始");
        }
        // TODO 缓存中并没有放已发行数量 这里是个bug 已有解决方案，但我这里直接注释了
//        if (coupon.getIssueNum() >= coupon.getTotalNum()) {
//            throw new BadRequestException("优惠券库存不足");
//        }

        Long userId = UserContext.getUser();
        String key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + couponId;
        Long count = stringRedisTemplate.opsForHash().increment(key, userId.toString(), 1);
        // 校验限领数量
        if(count > coupon.getUserLimit()){
            throw new BadRequestException("超出领取数量");
        }
        // 扣减优惠券库存
        stringRedisTemplate.opsForHash().increment(
                PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId, "totalNum", -1);



//        Long userId = UserContext.getUser();
//        // 组装lua脚本的参数
//        // 校验是否存在该优惠券，以及是否领完
//        String key1 = PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId.toString();
//        // 校验是否超领
//        String key2 = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + couponId.toString();
//        // 执行脚本 TODO bug Unknown redis exception; nested exception is java.lang.UnsupportedOperationException: io.lettuce.core.output.StatusOutput does not support set(long)
//        Integer r = stringRedisTemplate.execute(RECEIVE_COUPON_SCRIPT, List.of(key1, key2), userId.toString());
//        int result = NumberUtils.null2Zero(r);
//        if (result != 0) {
//            // 结果大于0，说明出现异常
//            throw new BizIllegalException(PromotionConstants.RECEIVE_COUPON_ERROR_MSG[result - 1]);
//        }


        // 发送MQ消息
        UserCouponDTO uc = new UserCouponDTO();
        uc.setUserId(userId);
        uc.setCouponId(couponId);
        mqHelper.send(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVE, uc);

    }


    /**
     * 兑换码兑换优惠券接口
     */
    @Override
//    @Transactional 使用lua脚本不加锁也是安全的
//    @Lock(name = "lock:coupon:#{T(com.tianji.common.utils.UserContext).getUser()}")
    public void exchangeCoupon(String code) {
        // 获取序列号
        long serialNum = CodeUtil.parseCode(code);
//        // 2.校验是否已经兑换 SETBIT KEY 4 1
//        boolean exchanged = codeService.updateExchangeMark(serialNum, true);
//        if (exchanged) {
//            throw new BizIllegalException("兑换码已经被兑换过了");
//        }
//        try {
//            // 3.查询兑换码对应的优惠券id
//            Long couponId = codeService.exchangeTargetId(serialNum);
//            if (couponId == null) {
//                throw new BizIllegalException("兑换码不存在！");
//            }
//            Coupon coupon = queryCouponByCache(couponId);
//            // 4.是否过期
//            LocalDateTime now = LocalDateTime.now();
//            if (now.isAfter(coupon.getIssueEndTime()) || now.isBefore(coupon.getIssueBeginTime())) {
//                throw new BizIllegalException("优惠券活动未开始或已经结束");
//            }
//
//            // 5.校验每人限领数量
//            Long userId = UserContext.getUser();
//            // 5.1.查询领取数量
//            String key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + couponId;
//            Long count = stringRedisTemplate.opsForHash().increment(key, userId.toString(), 1);
//            // 5.2.校验限领数量
//            if (count > coupon.getUserLimit()) {
//                throw new BadRequestException("超出领取数量");
//            }
//
//            // 6.发送MQ消息通知
//            UserCouponDTO uc = new UserCouponDTO();
//            uc.setUserId(userId);
//            uc.setCouponId(couponId);
//            uc.setSerialNum((int) serialNum);
//            mqHelper.send(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVE, uc);
//        } catch (Exception e) {
//            // 重置兑换的标记 0
//            codeService.updateExchangeMark(serialNum, false);
//            throw e;
//        }
        Long userId = UserContext.getUser();
        // 使用标记
        String key1 = COUPON_CODE_MAP_KEY;
        // 优惠券序列号范围
        String key2 = COUPON_RANGE_KEY;
        // 执行lua脚本 TODO String 永远是null  Unknown redis exception; nested exception is java.lang.UnsupportedOperationException: io.lettuce.core.output.StatusOutput does not support set(long)
        String result = stringRedisTemplate.execute(EXCHANGE_COUPON_SCRIPT,
                List.of(key1, key2),
                String.valueOf(serialNum), String.valueOf(serialNum + 5000), userId.toString());
        long r = NumberUtils.parseLong(result);
        if (r < 10) {
            // 异常结果应该是在1~5之间
            throw new BizIllegalException(PromotionConstants.EXCHANGE_COUPON_ERROR_MSG[(int) (r - 1)]);
        }
        // 发送MQ消息通知
        UserCouponDTO uc = new UserCouponDTO();
        uc.setUserId(userId);
        uc.setCouponId(r);
        uc.setSerialNum((int) serialNum);
        mqHelper.send(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVE, uc);
    }


    /**
     * 校验并 生成用户券
     */
//    @Lock(name = "lock:coupon:#{userId}") // 锁实现是基于切面的，该order是0，spring的事务的优先级是Integer的最大值优先级最低， SPEL表达式
    @Transactional
    @Override
    public void checkAndCreateUserCoupon(UserCouponDTO uc) {
        // 1.查询优惠券
        Coupon coupon = couponMapper.selectById(uc.getCouponId());
        if (coupon == null) {
            throw new BizIllegalException("优惠券不存在！");
        }
        // 2.更新优惠券的已经发放的数量 + 1
        int r = couponMapper.incrIssueNum(coupon.getId());
        if (r == 0) {
            throw new BizIllegalException("优惠券库存不足！");
        }
        // 3.新增一个用户券
        saveUserCoupon(coupon, uc.getUserId());
        // 4.更新兑换码状态
        if (uc.getSerialNum() != null) {
            codeService.lambdaUpdate()
                    .set(ExchangeCode::getUserId, uc.getUserId())
                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                    .eq(ExchangeCode::getId, uc.getSerialNum())
                    .update();
        }
    }

    /**
     * 新增一个用户券
     */
    private void saveUserCoupon(Coupon coupon, Long userId) {
        // 1.基本信息
        UserCoupon uc = new UserCoupon();
        uc.setUserId(userId);
        uc.setCouponId(coupon.getId());
        // 2.有效期信息
        LocalDateTime termBeginTime = coupon.getTermBeginTime();
        LocalDateTime termEndTime = coupon.getTermEndTime();
        if (termBeginTime == null) {
            termBeginTime = LocalDateTime.now();
            termEndTime = termBeginTime.plusDays(coupon.getTermDays());
        }
        uc.setTermBeginTime(termBeginTime);
        uc.setTermEndTime(termEndTime);
        // 3.保存
        save(uc);
    }

    /**
     * 查询redis中缓存的优惠券信息
     *
     * @param couponId 优惠券id
     * @return 优惠券实体
     */
    private Coupon queryCouponByCache(Long couponId) {
        String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId;
        Map<Object, Object> objectMap = stringRedisTemplate.opsForHash().entries(key);
        if (CollUtils.isEmpty(objectMap)) {
            return null;
        }
        // 数据反序列化
        return BeanUtils.mapToBean(objectMap, Coupon.class, false, CopyOptions.create());
    }

}
